package com.googlecode.archimedes.filegen.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

/**
 * <p>
 * Abstract super-class for Entities and Interfaces
 * </p>
 * 
 * <p>Created: Jun 22, 2005</p>
 * 
 * @author Erik Gustavson
 * @version $Id: AbstractType.java 2088 2006-03-16 01:56:24 +0000 (Thu, 16 Mar 2006) erik $
 */
public abstract class AbstractType extends AbstractAsset {

    private String              entityName       = "test";
    private String              displayName      = "TestDisplay";
    private String              extendsReference = null;
    protected Set<Member>       members          = new HashSet<Member>();
    protected Set<Relationship> relationships    = new HashSet<Relationship>();
    private String              description      = "No Description.";
    public Set<AbstractMethod>  abstractMethods  = new HashSet<AbstractMethod>();

    /**
     * Returns the AbstractType this AbstractType extends. Can be null if there is no
     * parent type.
     * 
     * @return
     */
    protected abstract AbstractType getExtendedType();

    /**
     * if true, also include inherited members.
     * @param includeInherited
     * @return
     */
    public Set<Member> getMembers(boolean includeInherited) {

        List<Member> results = new ArrayList<Member>(members);

        if ((includeInherited) && (getExtendedType() != null)) {
            results.addAll(getExtendedType().getMembers(includeInherited));
        }

        Collections.sort(results);

        return new LinkedHashSet<Member>(results);
    }

    /**
     * @see com.tps.filegen.model.AbstractType#getRelationships(boolean)
     */
    public Set<Relationship> getRelationships(boolean includeInherited) {
        List<Relationship> results = new ArrayList<Relationship>(relationships);

        if ((includeInherited) && (getExtendedType() != null)) {
            results.addAll(getExtendedType().getRelationships(includeInherited));
        }

        Collections.sort(results);

        return new LinkedHashSet<Relationship>(results);
    }

    /**
     * @return
     */
    public String getExtendsReference() {
        return extendsReference;
    }

    public void setExtendsReference(String arg0) {
        this.extendsReference = arg0;
    }

    /**
     * @return the EntityName with the first letter capitalized
     */
    public String getEntityName() {
        return entityName;
    }

    /**
     * @param entityName
     * automatically resets the string to cap firstletter, lowercase everything else
     */
    public void setEntityName(String entityName) {
        this.entityName = entityName;
    }

    /**
     * @return
     */
    public Set<Member> getMembers() {
        return getMembers(true);
    }

    /**
     * @param members
     */
    public void setMembers(Set<Member> members) {
        this.members = members;
    }

    public void addMember(Member member) {
        members.add(member);
    }

    /**
     * Loops through all relationships and pulls the entity that is NOT 
     * equal to the name of this Entity from any position in a relationship
     * @param
     * @return a list of Entities
     * TODO write a unit test
     */
    public List<AbstractType> getAllRelatedEntities() {

        List<AbstractType> allList = new ArrayList<AbstractType>();
        allList.addAll(this.getInboundRelatedEntities());
        allList.addAll(this.getOutboundRelatedEntities());
        return allList;
    }

    /**
     * Returns a list of relationships where this entity is the source entity.
     * @return
     */
    public List<Relationship> getSourceRelationships() {
        return getRelationships(End.Source);
    }

    /**
     * Returns a list of relationships where this entity is the target entity.
     * @return
     */
    public List<Relationship> getTargetRelationships() {
        return getRelationships(End.Target);
    }

    /**
     * Returns a list of relationships where this entity is either
     * on the Source or Target end of the relationship.
     * 
     * @param isSource true if this entity is the Source. False if this entity should be the target
     * 
     * @return a List<Relationship> of relationships
     */
    protected List<Relationship> getRelationships(End whichSide) {
        List<Relationship> results = new ArrayList<Relationship>();

        for (Relationship rel : getRelationships()) {

            AbstractType entityToTest = (whichSide == End.Source) ? rel.getFromEntity() : rel.getToEntity();
            try {
                //if the entityname to eval matches the current entity, we are good to go
                if (getEntityName().equals(entityToTest.getEntityName())) {
                    results.add(rel);

                } else if (matchesAParent(entityToTest)) {
                    results.add(rel);

                } else if ((entityToTest instanceof Interface) && (this instanceof Entity)) {
                    //perform an additional check - if the entityToTest is really and interface, see if the current entity
                    //implements that interface or not. If it does, we want to pass through the relationship as if it were
                    //our own.
                    if (((Entity) this).getInterfaces().contains(entityToTest)) {
                        results.add(rel);
                    }
                }
            } catch (Exception e) {
                log.info("FROM Entity: " + rel.getFromEntity());
                log.info("TO Entity: " + rel.getToEntity());
                log.info("Rel Type: " + rel.getClass());
                throw new RuntimeException("AbstractType [" + entityToTest + "] has problems on Relationship [" + rel + "] for direction [" + whichSide + "].", e);
            }
        }

        return results;
    }

    /**
     * Recursivly search up the extension tree for a matching entity. Return true
     * if one is found, otherwise return false.
     * 
     * @param entityToTest
     * @return
     */
    public boolean matchesAParent(AbstractType entityToTest) {
        if (this.getExtendedType() != null) {
            if (getExtendedType().getEntityName().equals(entityToTest.getEntityName())) { return true; }

            return getExtendedType().matchesAParent(entityToTest);
        }

        return false;
    }

    /**
     * Loops through all relationships and find those where <b>this</b> entity
     * is on the <b>target</b> end of the relationship. In other words, where
     * the relationship is <b>inbound</b> from this entity.
     * 
     * For example, if "Customers have Orders", the looking for source entities
     * on Order would return Customer.
     *    
     * @return a list of Entities where this entity is the target.
     */
    public Set<AbstractType> getInboundRelatedEntities() {
        return this.getRelatedEntities(End.Target);
    }

    /**
     * Loops through all relationships and find those where <b>this</b> entity
     * is on the <b>source</b> end of the relationship. In other words, where
     * the relationship is <b>outbound</b> from this entity.
     * 
     * For example, if "Customers have Orders", the looking for source entities
     * on Customer would return Order.
     *    
     * @return a list of Entities where this entity is the target.
     */
    public Set<AbstractType> getOutboundRelatedEntities() {
        return this.getRelatedEntities(End.Source);
    }

    /**
     * Loops through all relationships and pulls out the Entities that
     * are related to this entity on the specified side of the reltationship.
     * 
     * @param isSource true if you want Entities where this entity is the source. False is this entity is the target.
     */
    private Set<AbstractType> getRelatedEntities(End isSource) {
        //flip the isSource boolean because we want relationships where we are on one
        //side but then we want to pull the entity from the other side of the relationship.
        //If we did not flip the isSource boolean, we would always get back outselves.

        End end = (isSource == End.Source) ? End.Target : End.Source;

        return getEntitiesOutOfRelationships(getRelationships(isSource), end);
    }

    /**
     * Returns true if this entity has a parent class.
     * @return
     */
    public boolean isChild() {
        return !getParentRelationships().isEmpty();
    }

    /**
     * Returns a list of Entities where this entity is the parent entity.
     * 
     * @return true if the entityHas a Parent
     */
    public Set<AbstractType> getParentEntities() {
        return getEntitiesOutOfRelationships(getParentRelationships(), End.Source);
    }

    /**
     * Returns a list of Relationships where this entity is the source
     * of the relationship.
     * 
     * @return true if the entityHas a Parent
     */
    private List<Relationship> getParentRelationships() {

        List<Relationship> relationships = getRelationships(End.Target);
        Iterator<Relationship> iter = relationships.iterator();

        while (iter.hasNext()) {
            Relationship rel = iter.next();
            //technically, the LHS of a M:M is not a parent
            if (rel.getAssociation().equals(Association.ManyToMany)) {
                iter.remove();
            }
            if (rel.getAssociation().equals(Association.ManyToOne)) {
                iter.remove();
            }
        }

        return relationships;
    }

    /**
     * Utility method to Entities out of a List of relationships.
     * 
     * @param relationships The List of relationships to examine
     * @param isSource true if looking for entities on the source side, false if looking for entities on the target side
     * @return a List of Entities
     */
    private Set<AbstractType> getEntitiesOutOfRelationships(List<Relationship> relationships, End isSource) {
        Set<AbstractType> results = new HashSet<AbstractType>();

        for (Relationship rel : relationships) {
            AbstractType entity = (isSource == End.Source) ? rel.getFromEntity() : rel.getToEntity();
            results.add(entity);
        }

        return results;
    }

    /**
     * @return
     */
    public Set<Relationship> getRelationships() {
        return getRelationships(true);
    }

    /**
     * @param set the list of relationships
     */
    public void setRelationship(Set<Relationship> relationships) {
        this.relationships = relationships;
    }

    public void addRelationship(Relationship relationship) {
        relationships.add(relationship);
    }

    /**
     * @return
     */
    public String getDisplayName() {
        return displayName;
    }

    /**
     * @param displayName
     */
    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    /**
     * Print the Entity out as a String
     * 
     * @see java.lang.Object#toString()
     */
    public String toString() {
        return entityName;
    }

    /**
     * Returns the collection of members contained in this Entity.
     * 
     * @see com.tps.filegen.model.Asset#getSubordinateAssets()
     */
    public Collection<? extends Asset> getSubordinateAssets() {
        return this.getMembers();
    }

    /** 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof AbstractType == false) { return false; }
        if (this == obj) { return true; }

        AbstractType rhs = (AbstractType) obj;
        return new EqualsBuilder().append(entityName, rhs.entityName).isEquals();
    }

    /** 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        // you pick a hard-coded, randomly chosen, non-zero, odd number
        // ideally different for each class
        return new HashCodeBuilder(19, 29).append(entityName).toHashCode();
    }

    /**
     * Returns the description.
     *
     * @return String value of description
     */
    public String getDescription() {
        return description;
    }

    /**
     * Sets the description
     * @param description The description to set.
     */
    public void setDescription(String description) {
        this.description = description;
    }

    /**
     * Returns the abstractMethods.
     *
     * @return Set<AbstractMethod> value of abstractMethods
     */
    public Set<AbstractMethod> getAbstractMethods() {
        return abstractMethods;
    }

    /**
     * Sets the abstractMethods
     * @param abstractMethods The abstractMethods to set.
     */
    public void setAbstractMethods(Set<AbstractMethod> abstractMethods) {
        this.abstractMethods = abstractMethods;
    }

    /**
     * Sets the abstractMethods
     * @param abstractMethods The abstractMethods to set.
     */
    public void addAbstractMethod(AbstractMethod abstractMethod) {
        getAbstractMethods().add(abstractMethod);
    }
}
